package com.ebarlas.fractal.compile;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.ebarlas.fractal.compile.function.Abs;
import com.ebarlas.fractal.compile.function.Add;
import com.ebarlas.fractal.compile.function.And;
import com.ebarlas.fractal.compile.function.Divide;
import com.ebarlas.fractal.compile.function.Equals;
import com.ebarlas.fractal.compile.function.Exp;
import com.ebarlas.fractal.compile.function.GreaterThan;
import com.ebarlas.fractal.compile.function.GreaterThanEquals;
import com.ebarlas.fractal.compile.function.If;
import com.ebarlas.fractal.compile.function.LessThan;
import com.ebarlas.fractal.compile.function.LessThanEquals;
import com.ebarlas.fractal.compile.function.Log;
import com.ebarlas.fractal.compile.function.Mbrot;
import com.ebarlas.fractal.compile.function.Mod;
import com.ebarlas.fractal.compile.function.Multiply;
import com.ebarlas.fractal.compile.function.Negate;
import com.ebarlas.fractal.compile.function.Not;
import com.ebarlas.fractal.compile.function.NotEquals;
import com.ebarlas.fractal.compile.function.Or;
import com.ebarlas.fractal.compile.function.Power;
import com.ebarlas.fractal.compile.function.Square;
import com.ebarlas.fractal.compile.function.Subtract;
import com.ebarlas.fractal.util.Complex;


public class ParserImpl implements Parser {
    
    private static final String POS_REAL_REGEX = "(((\\d+)?(\\.\\d+))|(\\d+))";
    private static final Pattern POS_REAL_PATTERN = Pattern.compile(POS_REAL_REGEX);
    
    private static final String POS_IMAGINARY_REGEX = "("+POS_REAL_REGEX+"?i)";
    private static final Pattern POS_IMAGINARY_PATTERN = Pattern.compile(POS_IMAGINARY_REGEX);

    /**
     * Set of all terminals
     */
    private final List<Terminal> terminals;
    
    /**
     * List of token for current parsing session
     */
    private List<Token> tokens;
    private int _index;
    
    public ParserImpl() {
        terminals = new ArrayList<Terminal>();
        initTokens();
        initFunctions();
    }
    
    private void initFunctions() {
        addFunction("exp", Exp.getInstance());
        addFunction("abs", Abs.getInstance());
        addFunction("log", Log.getInstance());
        addFunction("if", If.getInstance());  
        addFunction("mbrot", Mbrot.getInstance());
        addFunction("square", Square.getInstance());
    }
    
    private void initTokens() {
        terminals.add(new Terminal(TerminalType.LPAREN, "\\("));
        terminals.add(new Terminal(TerminalType.RPAREN, "\\)"));
        terminals.add(new Terminal(TerminalType.COMMA, "\\,"));
        terminals.add(new Terminal(TerminalType.PLUS, "\\+", Add.getInstance()));
        terminals.add(new Terminal(TerminalType.MINUS, "\\-", Subtract.getInstance(), Negate.getInstance()));
        terminals.add(new Terminal(TerminalType.MUL, "\\*", Multiply.getInstance()));
        terminals.add(new Terminal(TerminalType.DIV, "\\/", Divide.getInstance()));
        terminals.add(new Terminal(TerminalType.MOD, "\\%", Mod.getInstance()));
        terminals.add(new Terminal(TerminalType.AND, "\\&\\&", And.getInstance()));
        terminals.add(new Terminal(TerminalType.OR, "\\|\\|", Or.getInstance()));
        terminals.add(new Terminal(TerminalType.EQ, "\\=\\=", Equals.getInstance()));
        terminals.add(new Terminal(TerminalType.NE, "\\!\\=", NotEquals.getInstance()));
        terminals.add(new Terminal(TerminalType.LT, "\\<", LessThan.getInstance()));
        terminals.add(new Terminal(TerminalType.GT, "\\>", GreaterThan.getInstance()));
        terminals.add(new Terminal(TerminalType.LE, "\\<\\=", LessThanEquals.getInstance()));
        terminals.add(new Terminal(TerminalType.GE, "\\>\\=", GreaterThanEquals.getInstance()));
        terminals.add(new Terminal(TerminalType.NOT, "\\!", Not.getInstance()));
        terminals.add(new Terminal(TerminalType.POW, "\\^", Power.getInstance()));
        terminals.add(new Terminal(TerminalType.REAL, POS_REAL_PATTERN));
        terminals.add(new Terminal(TerminalType.IMAGINARY, POS_IMAGINARY_PATTERN));
    }
    
    public void addFunction(String pattern, StackCommand function) {
        terminals.add(new Terminal(TerminalType.FUNC, pattern, function));
    }
    
    public void addVariable(String pattern, Variable var) {
        terminals.add(new Terminal(TerminalType.VAR, pattern, var));
    }
    
    public static Complex parseImaginary(String s) {
        if(s.equals("i")) {
            return Complex.I;
        } else {
            return new Complex(0, Double.parseDouble(s.substring(0, s.length()-1)));
        }
    }    
    
    public static Complex parseReal(String s) {
        return new Complex(Double.parseDouble(s));
    }
    
    private Token longestTokenMatch(String expr) {
        Token token = null;
        
        for(Terminal term : terminals) {
            Pattern pattern = term.getPattern();
            Matcher matcher = pattern.matcher(expr);
            if(matcher.lookingAt()) {
                String match = matcher.group();
                if(token == null || match.length() > token.getMatch().length()) {
                    token = new Token(term, match);
                }
            }
        }
        
        return token;
    }
    
    public List<Token> scan(String expr) throws ScanException {
        List<Token> tokens = new ArrayList<Token>(); 
        
        for(int i=0; i<expr.length();) {
            // consume whitespace
            if(Character.isWhitespace(expr.charAt(i))) {
                i++;
                continue;
            }
            
            String substr = expr.substring(i);
            Token longest = longestTokenMatch(substr);
            if(longest == null) {
                throw new ScanException("Remaining input: " + substr);
            } else {
                tokens.add(longest);
                i += longest.getMatch().length();
            }
        }
        
        return tokens;
    }    
    
    private Token nextToken() {
        Token token = getToken();
        _index++;
        return token;
    }    
    
    private Token getToken() {
        return tokens.get(_index);
    }    

    private boolean hasNext() {
        return _index < tokens.size();
    } 
    
    private boolean hasNext(TerminalType type) {
        return hasNext() && getToken().getTerminal().getType() == type;
    }
    
    private void consume(TerminalType type) throws ParseException {
        if(!hasNext(type))
            throw new ParseException();
        nextToken();
    }    
    
    private Literal nextLiteral() throws ParseException {
        if(hasNext(TerminalType.REAL))
            return new Literal(parseReal(nextToken().getMatch()));
        if(hasNext(TerminalType.IMAGINARY))
            return new Literal(parseImaginary(nextToken().getMatch()));        
        throw new ParseException();
    }
    
    private StackCommand nextFunction() throws ParseException {
        if(!hasNext(TerminalType.FUNC)) 
            throw new ParseException();
        return nextToken().getTerminal().getCommand();
    }
    
    private Variable nextVariable() throws ParseException {
        if(!hasNext(TerminalType.VAR)) 
            throw new ParseException();
        return (Variable)nextToken().getTerminal().getCommand();
    }
    
    public List<StackCommand> parse(List<Token> tokens) throws ParseException {
        this.tokens = tokens;
        _index = 0;
        List<StackCommand> result = new ArrayList<StackCommand>();
        start(result);
        if(hasNext())
            throw new ParseException();
        return result;
    }
    
    public void start(List<StackCommand> result) throws ParseException {
        expression(result);
    }
    
    private void expression(List<StackCommand> result) throws ParseException {
        orExpression(result);
    }    
    
    /**
     * OrExpression -> AndExpression ( ( <OR> AndExpression ) )*
     */
    private void orExpression(List<StackCommand> result) throws ParseException {
        andExpression(result);
        
        while(hasNext(TerminalType.OR)) {
            Token token = nextToken();
            andExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    /**
     * AndExpression -> EqualExpression ( ( <AND> EqualExpression ) )*
     */
    private void andExpression(List<StackCommand> result) throws ParseException {
        equalExpression(result);
        
        while(hasNext(TerminalType.AND)) {
            Token token = nextToken();
            equalExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    /**
     * EqualExpression -> RelationalExpression ( ( <NE> RelationalExpression ) | ( <EQ> RelationalExpression ) )* 
     */
    private void equalExpression(List<StackCommand> result) throws ParseException {
        relationalExpression(result);
        
        while(hasNext(TerminalType.EQ) || hasNext(TerminalType.NE)) {
            Token token = nextToken();
            relationalExpression(result);
            result.add(token.getTerminal().getCommand());
        }        
    }    
    
    /**
     * RelationalExpression -> AdditiveExpression ( ( <LT> AdditiveExpression ) | ( <GT> AdditiveExpression ) | ( <LE> AdditiveExpression ) | ( <GE> AdditiveExpression ) )*
     */
    private void relationalExpression(List<StackCommand> result) throws ParseException {
        additiveExpression(result);
        
        while(hasNext(TerminalType.LT) || hasNext(TerminalType.LE) || hasNext(TerminalType.GT) || hasNext(TerminalType.GE)) {
            Token token = nextToken();
            additiveExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    /**
     * AdditiveExpression -> MultiplicativeExpression ( (<PLUS> MultiplicativeExpression) | (<MINUS> MultiplicativeExpression) )*
     */
    private void additiveExpression(List<StackCommand> result) throws ParseException {
        multiplicativeExpression(result);
        
        while(hasNext(TerminalType.PLUS) || hasNext(TerminalType.MINUS)) {
            Token token = nextToken();
            multiplicativeExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    /**
     * MultiplicativeExpression -> UnaryExpression ( (PowerExpression) | (<MUL> UnaryExpression) | (<MOD> UnaryExpression) | (<DIV> UnaryExpression) )*
     */
    private void multiplicativeExpression(List<StackCommand> result) throws ParseException {
        unaryExpression(result);
        
        while(hasNext(TerminalType.MUL) || hasNext(TerminalType.DIV) || hasNext(TerminalType.MOD)) {
            Token token = nextToken();
            unaryExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    /**
     * UnaryExpression -> <PLUS> UnaryExpression | <MINUS> UnaryExpression | <NOT> UnaryExpression | PowerExpression
     */
    private void unaryExpression(List<StackCommand> result) throws ParseException {
        if(hasNext(TerminalType.PLUS)) {
            nextToken();
            unaryExpression(result);
        } else if(hasNext(TerminalType.MINUS)) {
            Token token = nextToken();
            unaryExpression(result);
            result.add(token.getTerminal().getCommand(1));
        } else if(hasNext(TerminalType.NOT)) {
            Token token = nextToken();
            unaryExpression(result);
            result.add(token.getTerminal().getCommand());
        } else if(hasNextPowerExpression()) {
            powerExpression(result);
        } else {
            throw new ParseException();
        }
    }    
    
    private boolean hasNextPowerExpression() throws ParseException {
        return hasNextUnaryExpressionNotPlusMinus();
    }    
    
    /**
     * PowerExpression -> UnaryExpressionNotPlusMinus ( <POWER> unaryExpression ) ?
     */
    private void powerExpression(List<StackCommand> result) throws ParseException {
        unaryExpressionNotPlusMinus(result);
        
        if(hasNext(TerminalType.POW)){
            Token token = nextToken();
            unaryExpression(result);
            result.add(token.getTerminal().getCommand());
        }
    }    
    
    private boolean hasNextUnaryExpressionNotPlusMinus() throws ParseException {
        return hasNext(TerminalType.REAL) || hasNext(TerminalType.IMAGINARY) || hasNext(TerminalType.FUNC) || hasNext(TerminalType.VAR) || hasNext(TerminalType.LPAREN);
    }    
    
    /**
     * UnaryExpressionNotPlusMinus -> Literal | Function | Variable | <LPAREN> Expression <RPAREN>
     */
    private void unaryExpressionNotPlusMinus(List<StackCommand> result) throws ParseException {
        if(hasNext(TerminalType.REAL) || hasNext(TerminalType.IMAGINARY)) {
            literal(result);
        } else if(hasNext(TerminalType.FUNC)) {
            function(result);
        } else if(hasNext(TerminalType.VAR)) {
            variable(result);
        } else if(hasNext(TerminalType.LPAREN)) {
            nextToken();
            expression(result);
            consume(TerminalType.RPAREN);
        } else {
            throw new ParseException();
        }
    }    
    
    private void variable(List<StackCommand> result) throws ParseException {
        result.add(nextVariable());
    }    
    
    private void function(List<StackCommand> result) throws ParseException {
        StackCommand function = nextFunction();
        int args = function.consumed();

        consume(TerminalType.LPAREN);
        
        for(int i=0; i<args; i++) {
            expression(result);
            
            if(i != args-1)
                consume(TerminalType.COMMA);
        }
        
        consume(TerminalType.RPAREN);
        
        result.add(function);
    }    
        
    private void literal(List<StackCommand> result) throws ParseException {
        result.add(nextLiteral());     
    }     
    
}
